package de.jmda.core.mproc;

import static de.jmda.core.mproc.ProcessingUtilities.getRootElements;
import static de.jmda.core.mproc.ProcessingUtilities.getSupportedAnnotations;
import static de.jmda.core.mproc.ProcessingUtilities.isProcessingOver;
import static de.jmda.core.util.StringUtil.sb;
import static java.lang.System.lineSeparator;

import java.io.File;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;

/**
 * Abstract base class for custom annotation processors. Instantiates {@link
 * ProcessingUtilities} singleton for each processing round.
 */
//@SupportedAnnotationTypes(value={"*"})
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public abstract class Processor
		extends AbstractProcessor
{
	private final static Logger LOGGER = Logger.getLogger(Processor.class);

	/**
	 * If the processor is started by {@link Launcher} this class variable will be
	 * populated with the files from the launcher's {@link LaunchConfiguration}.
	 * TODO does this have to be public final static?
	 */
	public final static List<File> FILES_TO_PROCESS = new ArrayList<>();

//	/**
//	 * Indicates if {@link #checkSupportedAnnotationTypesAnnotation()} had been
//	 * called already.
//	 */
//	private boolean passedSupportedAnnotationTypesAnnotationCheck = false;
//
//	/**
//	 * Indicates if {@link #checkSupposedAndActuallySupportedAnnotationTypes()}
//	 * had been called already.
//	 */
//	private boolean passedSupposedAndActuallySupportedAnnotationTypesCheck = false;
	
	/**
	 * Injected into processor instance once during initialisation.
	 * 
	 * @see Processor#init(ProcessingEnvironment)
	 */
	private ProcessingEnvironment processingEnvironment;

	// TODO add constructor with ProcessingTask parameter
	
	protected Processor()
	{
		// do not call instance methods from inside a constructor!
//		checkSupportedAnnotationTypesAnnotation();
//		checkSupposedAndActuallySupportedAnnotationTypes();
	}
	
	/**
	 * @return Subclasses provide this set of annotation types that the processor
	 *         is supposed to support. The {@link #Processor()} constructor uses
	 *         this array to verify that the processor's {@link
	 *         SupportedAnnotationTypes} annotation in fact describes all types
	 *         that are contained in this array.
	 *         <p>
	 *         This verification is very useful if the qualified name of an
	 *         annotation that is supposed to be supported is misspelled in the
	 *         processor's <code>SupportedAnnotationTypes</code> annotation or
	 *         changed (e. g. as a result of a refactoring).
	 *         <p>
	 *         Mismatches between a processor's <code>SupportedAnnotationTypes
	 *         </code> annotation and the supposed annotation types in this array
	 *         will be reported by {@link Processor#LOGGER}.
	 */
	protected abstract Set<Class<? extends Annotation>> getSupposedSupportedAnnotationTypes();

	/**
	 * Utility method for convenient creation of the return value for {@link
	 * #getSupposedSupportedAnnotationTypes()}
	 *
	 * @param elems annotation types
	 * @return a set of annotation types built from the varargs parameter value
	 *         <code>elems</code>
	 */
	@SafeVarargs
  protected static Set<Class<? extends Annotation>> asSet(
			Class<? extends Annotation> ... elems)
	{
		Set<Class<? extends Annotation>> result = new HashSet<>();
		
		for (Class<? extends Annotation> elem : elems)
		{
			result.add(elem);
		}
		
		return result;
	}

	/**
	 * Best effort approach to return a file containing the source code for <code>
	 * typeElement</code>.
	 * @param typeElement see below
	 * @return First file found in {@link #FILES_TO_PROCESS} which's absolute path
	 *         name ends with <code>typeElement</code>'s <code>toString()</code>
	 *         representation. If no such element could be found and <code>
	 *         typeElement</code> is an inner type the search continues for the
	 *         enclosing types. <code>null</code> if no such element could be
	 *         found for the outermost type element.
	 */
	public static File getSourceFile(TypeElement typeElement)
	{
		// TODO use utility method for this
		String sourceFileName =
				typeElement.toString().replace('.', File.separatorChar) + ".java";

		for (File file : FILES_TO_PROCESS)
		{
			if (file.getAbsolutePath().endsWith(sourceFileName))
			{
				return file;
			}
		}

		TypeElement enclosingTypeElement =
				ProcessingUtilities.getEnclosingTypeElement(typeElement);

		if (enclosingTypeElement != null)
		{
			return getSourceFile(enclosingTypeElement);
		}

		return null;
	}

	/**
	 * Will be called once by <code>javac</code>. Calls overridden method and
	 * stores processing environment locally.
	 */
	@Override
	public synchronized void init(ProcessingEnvironment processingEnvironment)
			throws ExceptionInInitializerError
	{
		super.init(processingEnvironment);
		this.processingEnvironment = processingEnvironment;

		checkSupportedAnnotationTypesAnnotation();
		checkSupposedAndActuallySupportedAnnotationTypes();
	}

	/**
	 * Will be called by {@link AbstractProcessor} once for every processing
	 * round. Instantiates {@link ProcessingUtilities} singleton and then calls
	 * {@link #process()}.
	 * 
	 * @return as defined in {@link AbstractProcessor#process(Set, RoundEnvironment)}
	 */
	@Override
	public boolean process(
			Set<? extends TypeElement> supportedAnnotations,
			RoundEnvironment roundEnvironment)
	{
//		if (false == passedSupportedAnnotationTypesAnnotationCheck)
//		{
//			checkSupportedAnnotationTypesAnnotation();
//		}

		ProcessingUtilities.instance(
				processingEnvironment, supportedAnnotations, roundEnvironment);

//		if (false == passedSupposedAndActuallySupportedAnnotationTypesCheck)
//		{
//			checkSupposedAndActuallySupportedAnnotationTypes();
//		}

    return process();
	}

	/**
	 * Implement this method to provide processing functionality.
	 *
	 * @return as defined in {@link AbstractProcessor#process(Set, RoundEnvironment)}
	 */
	protected abstract boolean process();

	/**
	 * @return string buffer representation of some details of processors internal
	 *         state
	 */
	protected StringBuffer processorStateAsStringBuffer()
	{
		StringBuffer result =
				new StringBuffer(
						"state of [" + getClass().getName() + "]\n" +
						"supported annotations\n");

		for(TypeElement typeElement : getSupportedAnnotations())
		{
			result.append("\t[" + typeElement.getQualifiedName() + "]\n");
		}

		result.append("root elements\n");

		for (Element element : getRootElements())
		{
			result.append("\t" + element.asType() + "\n");
		}

		return new StringBuffer(StringUtils.stripEnd(result.toString(), "\n"));
	}

	/**
	 * @throws ExceptionInInitializerError if
	 *         <ul>
	 *           <li>{@link SupportedAnnotationTypes} annotation is missing on
	 *               the processor's class declaration</li> or
	 *           <li><b><i>actually</i> supported annotation types</b> (from
	 *               {@link #supportedAnnotations}) is empty</li>
	 *         </ul>
	 */
	private void checkSupportedAnnotationTypesAnnotation()
	{
		// type names from the SupportedAnnotationTypes annotation value
		Set<String> supportedAnnotationTypeNames = getSupportedAnnotationTypes();

		// check if there are supported annotation type names
		if (supportedAnnotationTypeNames.size() == 0)
		{
			// no supported annotation type names, try to find out the reason
			SupportedAnnotationTypes supportedAnnotationTypesAnnotation =
					getClass().getAnnotation(SupportedAnnotationTypes.class);

			if (supportedAnnotationTypesAnnotation == null)
			{
				// there is no SupportedAnnotationTypes annotation on the processor
				// class declaration
				throw new ExceptionInInitializerError(
						"missing " + SupportedAnnotationTypes.class.getName() +
						" annotation in " + getClass().getName());
			}

			// there is a SupportedAnnotationTypes annotation on the processor's class
			// declaration but there is no or an empty value attribute in the
			// annotation
			throw new ExceptionInInitializerError(
					"missing " + SupportedAnnotationTypes.class.getName() +
					" values in " + getClass().getName());
		}

		// check supported annotation type names
		for (String supportedAnnotationTypeName : supportedAnnotationTypeNames)
		{
			if (false == supportedAnnotationTypeName.contains("*"))
			{
				// try loading the annotation type
				try
				{
					Class.forName(supportedAnnotationTypeName);
				}
				catch (ClassNotFoundException e)
				{
					// do not throw exception here, just log warning
					// maybe the supported annotation type is simply not relevant for
					// the currently processed code base
					LOGGER.warn(
							SupportedAnnotationTypes.class.getName() + " values contain [" +
									supportedAnnotationTypeName + "] which can not be found\n" + e);
				}
			}
		}

		// check the annotation types which are supposed to be supported
		if (getSupposedSupportedAnnotationTypes().size() == 0)
		{
			LOGGER.warn("missing supposed supported annotation types");
		}
		else
		{
			Set<String> supposedSupportedAnnotationTypeNames =
					getSupposedSupportedAnnotationTypeNames();

			for (String supposedSupportedAnnotationTypeName :
			     supposedSupportedAnnotationTypeNames)
			{
				if (false ==
						supportedAnnotationTypeNames.contains(
								supposedSupportedAnnotationTypeName))
				{
					LOGGER.warn(
							"[" + supposedSupportedAnnotationTypeName +
							"] is supposed to be supported but might not be contained in " +
							"the actually supported annotation types of [" +
							getClass().getName() + "]");
				}
			}
		}

		// if no runtime exception was thrown check was passed
//		passedSupportedAnnotationTypesAnnotationCheck = true;
	}

	private void checkSupposedAndActuallySupportedAnnotationTypes()
	{
		// build string set and stringbuffer from names of actually supported
		// annotation types
		Set<String> actuallySupportedAnnotationTypeNames = new HashSet<String>();

		StringBuffer actuallySupportedAnnotationTypeNamesAsStringBuffer =
				new StringBuffer("\n");

		for (String actuallySupportedAnnotationTypeQualifiedName :
		     getSupportedAnnotationTypes())
		{
			actuallySupportedAnnotationTypeNames.add(
					actuallySupportedAnnotationTypeQualifiedName);
			actuallySupportedAnnotationTypeNamesAsStringBuffer.append(
					actuallySupportedAnnotationTypeQualifiedName + "\n");
		}

		StringBuffer supposedSupportedAnnotationTypeNamesAsStringBuffer =
				sb(lineSeparator());

		boolean foundMissing = false;

		// check if each supposed supported annotation type is contained in the
		// list of actually supported annotation types
		for (Class<?> supposedSupportedAnnotationType :
		     getSupposedSupportedAnnotationTypes())
		{
			supposedSupportedAnnotationTypeNamesAsStringBuffer.append(
					supposedSupportedAnnotationType.getName() + "\n");

			if (false ==
					actuallySupportedAnnotationTypeNames.contains(
							supposedSupportedAnnotationType.getName()))
			{
				foundMissing = true;
			}
		}

		if (foundMissing && (false == isProcessingOver()))
		{
			LOGGER.error(
					"\n[" + getClass().getName() + "] " +
							"missing actually supported annotation type which is supposed to " +
							"be supported" +
							"\nactually supported annotation types" +
							actuallySupportedAnnotationTypeNamesAsStringBuffer.toString() +
							"\nsupposed supported annotation types" +
							supposedSupportedAnnotationTypeNamesAsStringBuffer.toString());
		}

//		passedSupposedAndActuallySupportedAnnotationTypesCheck = true;
	}

	private Set<String> getSupposedSupportedAnnotationTypeNames()
	{
		Set<String> result = new HashSet<String>();

		for (Class<?> supposedSupportedAnnotationType :
		     getSupposedSupportedAnnotationTypes())
		{
			result.add(supposedSupportedAnnotationType.getName());
		}

		return result;
	}
}